﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Xml.Linq;
using CoreEx.Common.Serialization;
using CoreEx.DynamicInvoker;
using LinFu.IoC.Configuration;

namespace CoreEx.Common.Implementation.Linq
{
    [Implements(typeof (IExpressionSerializer))]
    public class ExpressionSerializer : IExpressionSerializer
    {
        private Dictionary<string, ParameterExpression> parameters = new Dictionary<string, ParameterExpression>();
        
        public XElement Serialize(Expression expression)
        {
            var localExpression = Evaluator.PartialEval(expression);
            
            return VisitExpression(localExpression);
        }

        public Expression Deseserialize(XElement xmlExpression)
        {
            parameters.Clear();
            return VisitXmlExpression(xmlExpression);
        }

        protected XElement VisitExpression(Expression expression)
        {
            
            
            
            
            switch (expression.NodeType)
            {
                case ExpressionType.Lambda:
                    return VisitLambda((LambdaExpression) expression);
                case ExpressionType.Add:
                case ExpressionType.Equal:
                    return VisitBinary((BinaryExpression) expression);
                case ExpressionType.Parameter:
                    return VisitParameter((ParameterExpression) expression);
                case ExpressionType.MemberAccess:
                    return VisitMember((MemberExpression) expression);
                case ExpressionType.Constant:
                    return VisitConstant((ConstantExpression) expression);
                case ExpressionType.Call:
                    return VisitMethodCall((MethodCallExpression) expression);
            }
            return null;
        }


        protected Expression VisitXmlExpression(XElement xmlExpression)
        {
            switch (xmlExpression.Name.LocalName)
            {
                case "LambdaExpression":
                    return ParseLambdaExpression(xmlExpression);
                case "BinaryExpression":
                    return ParseBinaryExpression(xmlExpression);
                case "ParameterExpression":
                    return ParseParameterExpression(xmlExpression);
                case "ConstantExpression":
                    return ParseConstantExpression(xmlExpression);
            }

            return null;
        }


        protected LambdaExpression ParseLambdaExpression(XElement xmlLambdaExpression)
        {
            Expression body = VisitXmlExpression(xmlLambdaExpression.Element("Body").Elements().First());
            IEnumerable<ParameterExpression> parameters =
                xmlLambdaExpression.Element("Parameters").Elements().Select(p => ParseParameterExpression(p));
            Type type = ResolveTypeFromXml(xmlLambdaExpression.Element("Type"));
            return Expression.Lambda(type,body, parameters);
        }

        protected BinaryExpression ParseBinaryExpression(XElement xmlBinaryExpression)
        {
            Expression left = VisitXmlExpression(xmlBinaryExpression.Element("Left").Elements().First());
            Expression right = VisitXmlExpression(xmlBinaryExpression.Element("Right").Elements().First());
            var binaryType =
                (ExpressionType) Enum.Parse(typeof (ExpressionType), xmlBinaryExpression.Attribute("NodeType").Value);
            return Expression.MakeBinary(binaryType, left, right);
        }

        protected ParameterExpression ParseParameterExpression(XElement xmlParameterExpression)
        {
            Type type = ResolveTypeFromXml(xmlParameterExpression.Element("Type"));
            string name = xmlParameterExpression.Element("Name").Value;
            if (!parameters.ContainsKey(name))
                parameters.Add(name,Expression.Parameter(type,name));

            return parameters[name];
        }

        protected ConstantExpression ParseConstantExpression(XElement xmlConstantExpression)
        {
            Type type = ResolveTypeFromXml(xmlConstantExpression.Element("Type"));
            object value = Convert.ChangeType(xmlConstantExpression.Element("Value").Value, type);
            return Expression.Constant(value, type);
        }


        protected XElement VisitLambda(LambdaExpression lambdaExpression)
        {
            return new XElement("LambdaExpression",
                                new XAttribute("NodeType", lambdaExpression.NodeType.ToString()),
                                CreateTypeXml(lambdaExpression.Type),
                                new XElement("Body", VisitExpression(lambdaExpression.Body)),
                                new XElement("Parameters", lambdaExpression.Parameters.Select(p => VisitParameter(p))));
        }

        protected XElement VisitBinary(BinaryExpression binaryExpression)
        {
            return new XElement("BinaryExpression",
                                new XAttribute("NodeType", binaryExpression.NodeType.ToString()),
                                CreateTypeXml(binaryExpression.Type),
                                new XElement("Left", VisitExpression(binaryExpression.Left)),
                                new XElement("Right", VisitExpression(binaryExpression.Right)));
        }

        protected XElement VisitParameter(ParameterExpression parameterExpression)
        {
            return new XElement("ParameterExpression",
                                new XAttribute("NodeType", parameterExpression.NodeType.ToString()),
                                CreateTypeXml(parameterExpression.Type),
                                new XElement("Name", parameterExpression.Name));
        }

        protected XElement VisitMember(MemberExpression memberExpression)
        {
            var expression = (memberExpression.Expression as ConstantExpression);
            if (expression != null)
            {
                object value = expression.Value.GetType().DynamicInvoke(expression.Value, memberExpression.Member.Name);
                return VisitExpression(Expression.Constant(value));
            }
            return VisitExpression(memberExpression.Expression);
        }

        protected XElement VisitConstant(ConstantExpression constantExpression)
        {
            return new XElement("ConstantExpression",
                                new XAttribute("NodeType", constantExpression.NodeType.ToString()),
                                CreateTypeXml(constantExpression.Type),
                                new XElement("Value", constantExpression.Value));
        }


        protected XElement VisitMethodCall(MethodCallExpression methodCallExpression)
        {
            return null;
        }

        protected Type ResolveTypeFromXml(XElement typeElement)
        {           
            
            var typeName = typeElement.Attribute("Name").Value;            
            Type type = Type.GetType(typeName);
            if (type == null)                
            foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
            {
                type = assembly.GetType(typeName,false,true);
                if (type != null)
                    break;
            }

            if (type == null)
                throw new ArgumentException("Type not found");


            if (typeElement.HasElements)
            {
                var typeParameters =  typeElement.Element("GenericTypeParameters").Elements().Select(p => ResolveTypeFromXml(p));
                return type.MakeGenericType(typeParameters.ToArray());
            }
            return type;
        }


        protected XElement CreateTypeXml(Type type)
        {
            if (type.IsGenericType)
            {
                return new XElement("Type",
                                    new XAttribute("Name", type.GetGenericTypeDefinition().FullName),
                                    new XElement("GenericTypeParameters",
                                                 type.GetGenericArguments().Select(a => CreateTypeXml(a))));
            }
            return new XElement("Type", new XAttribute("Name", type.FullName));
        }
    }
}
